/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.skatiger.jrove.impl;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.net.URL;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import javax.el.ELException;

import org.skatiger.jrove.Jrove;
import org.skatiger.jrove.JroveContext;
import org.skatiger.jrove.JroveException;
import org.skatiger.jrove.JroveHandler;
import org.skatiger.jrove.ViewContext;
import org.skatiger.jrove.component.UIComponent;
import org.skatiger.jrove.util.ComponentSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

final class DefaultJrove implements Jrove {
	private static final Logger logger = LoggerFactory
			.getLogger(DefaultJrove.class);
	private final static String APPLIED_KEY = "~jrove.view.APPLIED~";
	private final String _alias;

	private final DefaultJroveFactory _factory;

	private final long _createTime;
	private final long _refreshPeriod;
	private final Map<String, URL> _relativePaths;

	private final JroveHandler _root;

	private final URL _src;

	public DefaultJrove(DefaultJroveFactory _factory, URL _src, String _alias,
			JroveHandler _root) {
		this._factory = _factory;
		this._src = _src;
		this._root = _root;
		this._alias = _alias;
		this._createTime = System.currentTimeMillis();
		this._relativePaths = new WeakHashMap<String, URL>();
		this._refreshPeriod = _factory.getRefreshPeriod();
		if (logger.isDebugEnabled()) {
			logger.debug("create Jrove with file " + _src + " at "
					+ new Date(_createTime));
		}
	}

	public void apply(ViewContext viewContext, UIComponent parent)
			throws IOException, JroveException, ELException {
		DefaultJroveContext ctx = new DefaultJroveContext(viewContext, this);
		ComponentSupport.markForDeletion(parent);
		this._root.apply(ctx, parent);
		ComponentSupport.finalizeForDeletion(parent);
	}

	/**
	 * Return the alias name for error messages and logging
	 * 
	 * @return alias name
	 */
	public String getAlias() {
		return this._alias;
	}

	/**
	 * The time when this Jrove was created, NOT the URL source code
	 * 
	 * @return final timestamp of when this Jrove was created
	 */
	public long getCreateTime() {
		return this._createTime;
	}

	/**
	 * Delegates resolution to DefaultJroveFactory reference. Also, caches URLs
	 * for relative paths.
	 * 
	 * @param path
	 *            a relative url path
	 * @return URL pointing to destination
	 * @throws IOException
	 *             if there is a problem creating the URL for the path specified
	 */
	private URL getRelativePath(String path) throws IOException {
		URL url = (URL) this._relativePaths.get(path);
		if (url == null) {
			url = this._factory.resolveURL(this._src, path);
			this._relativePaths.put(path, url);
		}
		return url;
	}

	/**
	 * The URL this Jrove was created from.
	 * 
	 * @return the URL this Jrove was created from
	 */
	public URL getSource() {
		return this._src;
	}

	/**
	 * Given the passed JroveContext, apply our child JroveHandlers to the
	 * passed parent
	 * 
	 * @see JroveHandler#apply(JroveContext, UIComponent)
	 * @param ctx
	 *            the JroveContext to use for applying our JroveHandlers
	 * @param parent
	 *            the parent component to apply changes to
	 * @throws IOException
	 * @throws JroveException
	 * @throws JroveException
	 * @throws ELException
	 */
	private void include(DefaultJroveContext ctx, UIComponent parent)
			throws IOException, JroveException, ELException {
		this._root.apply(new DefaultJroveContext(ctx, this), parent);
	}

	/**
	 * Used for delegation by the DefaultJroveContext. First pulls the URL from
	 * {@link #getRelativePath(String) getRelativePath(String)}, then calls
	 * {@link #include(JroveContext, UIComponent, URL) include(JroveContext,
	 * UIComponent, URL)}.
	 * 
	 * @see JroveContext#includeJrove(UIComponent, String)
	 * @param ctx
	 *            JroveContext to pass to the included Jrove
	 * @param parent
	 *            UIComponent to apply changes to
	 * @param path
	 *            relative path to the desired Jrove from the JroveContext
	 * @throws IOException
	 * @throws JroveException
	 * @throws JroveException
	 * @throws ELException
	 */
	public void include(DefaultJroveContext ctx, UIComponent parent, String path)
			throws IOException, JroveException, ELException {
		URL url = this.getRelativePath(path);
		this.include(ctx, parent, url);
	}

	/**
	 * Grabs a DefaultJrove from referenced DefaultJroveFacotry
	 * 
	 * @see DefaultJroveFactory#getJrove(URL)
	 * @param ctx
	 *            JroveContext to pass to the included Jrove
	 * @param parent
	 *            UIComponent to apply changes to
	 * @param url
	 *            URL source to include Jrove from
	 * @throws IOException
	 * @throws JroveException
	 * @throws ELException
	 */
	public void include(DefaultJroveContext ctx, UIComponent parent, URL url)
			throws IOException, JroveException, ELException {
		DefaultJrove f = (DefaultJrove) this._factory.getJrove(url);
		f.include(ctx, parent);
	}

	@Override
	public String toString() {
		return this._alias;
	}

	private final void refresh(UIComponent c) {
		if (_refreshPeriod > 0) {

			// finally remove any children marked as deleted
			int sz = c.getChildCount();
			if (sz > 0) {
				UIComponent cc = null;
				List<UIComponent> cl = c.getChildren();
				ApplyToken token;
				while (--sz >= 0) {
					cc = cl.get(sz);
					if (!cc.isTransient()) {
						token = (ApplyToken) cc.getAttributes()
								.get(APPLIED_KEY);
						if (token != null && token._time < _createTime
								&& token._alias.equals(_alias)) {
							if (logger.isDebugEnabled()) {
								logger.debug("Jrove[" + _alias
										+ "] was modified @ "
										+ new Date(_createTime)
										+ ", flushing component applied @ "
										+ new Date(token._time));
							}
							cl.remove(sz);
						}
					}
				}
			}

			// remove any facets marked as deleted
			if (c.getFacets().size() > 0) {
				Collection<UIComponent> col = c.getFacets().values();
				UIComponent fc;
				ApplyToken token;
				for (Iterator<UIComponent> itr = col.iterator(); itr.hasNext();) {
					fc = itr.next();
					if (!fc.isTransient()) {
						token = (ApplyToken) fc.getAttributes()
								.get(APPLIED_KEY);
						if (token != null && token._time < _createTime
								&& token._alias.equals(_alias)) {
							if (logger.isInfoEnabled()) {
								logger.info("Jrove[" + _alias
										+ "] was modified @ "
										+ new Date(_createTime)
										+ ", flushing component applied @ "
										+ new Date(token._time));
							}
							itr.remove();
						}
					}
				}
			}
		}
	}

	private static class ApplyToken implements Externalizable {
		public String _alias;

		public long _time;

		public ApplyToken(String alias, long time) {
			_alias = alias;
			_time = time;
		}

		public void readExternal(ObjectInput in) throws IOException,
				ClassNotFoundException {
			_alias = in.readUTF();
			_time = in.readLong();
		}

		public void writeExternal(ObjectOutput out) throws IOException {
			out.writeUTF(_alias);
			out.writeLong(_time);
		}
	}
}
